package cn.cloud.all.security.authentication.filter;

import cn.cloud.all.security.authentication.*;
import cn.cloud.all.security.core.Authentication;
import cn.cloud.all.security.core.AuthenticationException;
import cn.cloud.all.security.core.context.SecurityContext;
import cn.cloud.all.security.core.context.SecurityContextHolder;
import cn.cloud.all.security.web.util.matcher.AnyRequestMatcher;
import cn.cloud.all.security.web.util.matcher.RequestMatcher;
import lombok.Data;
import org.springframework.http.HttpStatus;
import org.springframework.util.Assert;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

public class AuthenticationFilter extends OncePerRequestFilter {

    private RequestMatcher requestMatcher = AnyRequestMatcher.INSTANCE;
    private AuthenticationConverter authenticationConverter;
    private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
    private AuthenticationFailureHandler failureHandler = new AuthenticationEntryPointFailureHandler(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED));
    private AuthenticationManagerResolver<HttpServletRequest> authenticationManagerResolver;

    public AuthenticationFilter(AuthenticationManager authenticationManager, AuthenticationConverter authenticationConverter) {
        this((AuthenticationManagerResolver<HttpServletRequest>) r -> authenticationManager, authenticationConverter);
    }

    public AuthenticationFilter(AuthenticationManagerResolver<HttpServletRequest> authenticationManagerResolver, AuthenticationConverter authenticationConverter) {
        Assert.notNull(authenticationManagerResolver, "authenticationManagerResolver cannot be null");
        Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");

        this.authenticationManagerResolver = authenticationManagerResolver;
        this.authenticationConverter = authenticationConverter;
    }

    public RequestMatcher getRequestMatcher() {
        return this.requestMatcher;
    }

    public void setRequestMatcher(RequestMatcher requestMatcher) {
        Assert.notNull(requestMatcher, "requestMatcher cannot be null");
        this.requestMatcher = requestMatcher;
    }

    public AuthenticationConverter getAuthenticationConverter() {
        return this.authenticationConverter;
    }

    public void setAuthenticationConverter(AuthenticationConverter authenticationConverter) {
        Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
        this.authenticationConverter = authenticationConverter;
    }

    public AuthenticationSuccessHandler getSuccessHandler() {
        return this.successHandler;
    }

    public void setSuccessHandler(AuthenticationSuccessHandler successHandler) {
        Assert.notNull(successHandler, "successHandler cannot be null");
        this.successHandler = successHandler;
    }

    public AuthenticationFailureHandler getFailureHandler() {
        return this.failureHandler;
    }

    public void setFailureHandler(AuthenticationFailureHandler failureHandler) {
        Assert.notNull(failureHandler, "failureHandler cannot be null");
        this.failureHandler = failureHandler;
    }

    public AuthenticationManagerResolver<HttpServletRequest> getAuthenticationManagerResolver() {
        return this.authenticationManagerResolver;
    }

    public void setAuthenticationManagerResolver(AuthenticationManagerResolver<HttpServletRequest> authenticationManagerResolver) {
        Assert.notNull(authenticationManagerResolver, "authenticationManagerResolver cannot be null");
        this.authenticationManagerResolver = authenticationManagerResolver;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        if (!this.requestMatcher.matches(request)) {
            filterChain.doFilter(request, response);
            return;
        }

        try {
            Authentication authenticationResult = attemptAuthentication(request, response);
            if (authenticationResult == null) {
                filterChain.doFilter(request, response);
                return;
            }

            HttpSession session = request.getSession(false);
            if (session != null) {
                request.changeSessionId();
            }

            successfulAuthentication(request, response, filterChain, authenticationResult);
        } catch (AuthenticationException e) {
            unsuccessfulAuthentication(request, response, e);
        }
    }

    private void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        SecurityContextHolder.clearContext();
        this.failureHandler.onAuthenticationFailure(request, response, failed);
    }

    private void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        context.setAuthentication(authentication);
        SecurityContextHolder.setContext(context);
        this.successHandler.onAuthenticationSuccess(request, response, chain, authentication);
    }

    private Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, ServletException {
        Authentication authentication = this.authenticationConverter.convert(request);
        if (authentication == null) {
            return null;
        }

        AuthenticationManager authenticationManager = this.authenticationManagerResolver.resolve(request);
        Authentication authenticationResult = authenticationManager.authenticate(authentication);
        if (authenticationResult == null) {
            throw new ServletException("AuthenticationManager should not return null Authentication object.");
        }

        return authenticationResult;
    }
}
